/* @flow */

import runInVm from './run-in-vm'
import type { Renderer, RenderOptions } from './create-renderer'
import { createSourceMapConsumers, rewriteErrorTrace } from './source-map-support'

const PassThrough = require('stream').PassThrough

const INVALID_MSG =
  'Invalid server-rendering bundle format. Should be a string ' +
  'or a bundle Object of type:\n\n' +
`{
  entry: string;
  files: { [filename: string]: string; };
  maps: { [filename: string]: string; };
}\n`

// The render bundle can either be a string (single bundled file)
// or a bundle manifest object generated by vue-ssr-webpack-plugin.
type RenderBundle = {
  entry: string;
  files: { [filename: string]: string; };
  maps: { [filename: string]: string; };
};

export function createBundleRendererCreator (createRenderer: () => Renderer) {
  return (bundle: string | RenderBundle, rendererOptions?: RenderOptions) => {
    const renderer = createRenderer(rendererOptions)
    let files, entry, maps
    if (typeof bundle === 'object') {
      entry = bundle.entry
      files = bundle.files
      maps = createSourceMapConsumers(bundle.maps)
      if (typeof entry !== 'string' || typeof files !== 'object') {
        throw new Error(INVALID_MSG)
      }
    } else if (typeof bundle === 'string') {
      entry = '__vue_ssr_bundle__'
      files = { '__vue_ssr_bundle__': bundle }
      maps = {}
    } else {
      throw new Error(INVALID_MSG)
    }
    return {
      renderToString: (context?: Object, cb: (err: ?Error, res: ?string) => void) => {
        if (typeof context === 'function') {
          cb = context
          context = {}
        }
        runInVm(entry, files, context).catch(err => {
          rewriteErrorTrace(err, maps)
          cb(err)
        }).then(app => {
          if (app) {
            renderer.renderToString(app, (err, res) => {
              rewriteErrorTrace(err, maps)
              cb(err, res)
            })
          }
        })
      },
      renderToStream: (context?: Object) => {
        const res = new PassThrough()
        runInVm(entry, files, context).catch(err => {
          rewriteErrorTrace(err, maps)
          // avoid emitting synchronously before user can
          // attach error listener
          process.nextTick(() => {
            res.emit('error', err)
          })
        }).then(app => {
          if (app) {
            const renderStream = renderer.renderToStream(app)
            renderStream.on('error', err => {
              rewriteErrorTrace(err, maps)
              res.emit('error', err)
            })
            renderStream.pipe(res)
          }
        })
        return res
      }
    }
  }
}
